<html>
<head>
<meta charset="UTF-8">
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/mathjs/5.4.0/math.min.js"></script>
</head>
<body>
<div>
	<h2>光线跟踪技术 练习</h2>
	<h4>镜面</h4>
	<br>
	<canvas id="canvas0" height="240" width="480">你的浏览器不支持canvas！</canvas>
</div>
<script>

	function unitVector(vec) {
		return math.divide(vec, math.norm(vec))
	}
	
	
	/*
	* 计算一个随机的单位向量，该单位向量服从于特定的随机分布。
	*/
	function randomUnitVector() {
		const a = Math.random() * 2 * Math.PI;  // xy平面上的角度服从于均匀随机分布
		const z = Math.random() * 2 - 1;  // 高度服从均匀随机分布
		const r = math.sqrt(1 - z*z);          // 计算该高度下圆截面的半径
		return [r * math.cos(a), r * math.sin(a), z]
	}
	
	
	function reflect(vec, normal) {
		return math.subtract(vec, math.multiply(2, math.dot(vec, normal), normal))
	}


	function color(ray, hitable, depth) {
		let hit_record = hitable.hit(ray, 1e-12, Number.MAX_VALUE)
		if (hit_record) {
			const material = hit_record.material
			const res = material.scatter(ray, hit_record)
			if (depth < 50 && res) {
				const scattered = res['scattered']
				const attenuation = res['attenuation']
				return math.dotMultiply(attenuation, color(scattered, hitable, depth + 1))
			}
			return [1e-2, 1e-2, 1e-2]
		} else {
			const direction = ray.direction
			const t = math.multiply(0.5, direction[1] + 1)
			return math.add(math.multiply(1 - t, [1, 1, 1]), math.multiply(t, [0.5, 0.7, 1]))
		}
	}
	
	
	// 散射体（材质）
	class Lambertian {
		constructor(a) {
			this.albedo = a
		}
		
		scatter(ray, hit_record) {
			const target = math.add(hit_record.point, hit_record.normal, randomUnitVector())
			const scattered = new Ray(hit_record.point, math.subtract(target, hit_record.point))
			const attenuation = this.albedo
			return {
				'attenuation': attenuation,
				'scattered': scattered,
			}
		}
	}
	
	
	// 金属（材质）
	class Metal {
		constructor(a) {
			this.albedo = a
		}
		
		scatter(ray_in, hit_record) {
			const reflected = reflect(unitVector(ray_in.direction), hit_record.normal)
			const scattered = new Ray(hit_record.point, reflected)
			const attenuation = this.albedo
			if (math.dot(scattered.direction, hit_record.normal) > 0)
				return {
					'attenuation': attenuation,
					'scattered': scattered
				}
			return null
		}
	}

	class Ray{
		constructor(origin, direction){
			this.origin = origin
			this.direction = unitVector(direction)
		}

		point_at_t(t){
			return math.add(this.origin, math.multiply(this.direction, t))
		}
	}
	
	
	class Camera{
		constructor(){
			this.start = [-2, -1, -1]
			this.horizontal = [4, 0, 0]
			this.vertical = [0, 2, 0]
			this.origin = [0, 0, 0]
		}
		
		getRay(u, v) {
			return new Ray(this.origin, math.add(math.add(this.start, math.multiply(u, this.horizontal)), math.subtract(math.multiply(v, this.vertical), this.origin)))
		}
	}
	

	class Sphere{
		constructor(center, radius, material) {
			this.center = center
			this.radius = radius
			this.material = material
		}

		hit(ray, tmin, tmax) {
			const oc = math.subtract(ray.origin, this.center)
			const a = math.dot(ray.direction, ray.direction)
			const b = 2 * math.dot(oc, ray.direction)
			const c = math.dot(oc, oc) - this.radius**2
			const d = b**2 - 4 * a * c
			const material = this.material
			if (d > 0) {  // 方程有解即交点存在
				function record(parameter, center, radius) {
					if (parameter < tmax && parameter > tmin) {
						let point = ray.point_at_t(parameter)
						let hit_record = {
							t: parameter,
							point: point,
							normal: math.divide(math.subtract(point, center), radius),
							material: material
						}
						return hit_record
					}
					return null
				}
				let hit_record = record((-b - math.sqrt(d)) / 2 / a, this.center, this.radius)
				if(hit_record) return hit_record
				hit_record = record((-b + math.sqrt(d)) / 2 / a, this.center, this.radius)
				if(hit_record) return hit_record
			}
			return null
		}
	}

	/* 一个无穷大的平面 */
	class Plane{
		// 平面方程ax + by + cz + d = 0
		constructor(a, b, c, d, material) {
			this.a = a;
			this.b = b;
			this.c = c;
			this.d = d;
			this.parameters = [a, b, c, d];
			this.normalVector = unitVector([a, b, c]);
			this.material = material;
		}

		hit(ray, tmin, tmax) {
			// 如果光线与平面法向量垂直，则不交
			if (math.abs(math.multiply(this.normalVector, ray.direction)) < 1e-8)
				return null;
			//
			let t = - (this.d + math.multiply(ray.origin, this.normalVector)) 
				/ math.multiply(ray.direction, this.normalVector);
			if (t <= tmin || t >= tmax)
				return null;
			return {
				t: t,
				point: ray.point_at_t(t),
				  //< 如果法向量固定的话，只有一面可以反射，另一面是黑的。
				normal: math.multiply(this.normalVector, ray.direction) < 0 ? this.normalVector : math.subtract(0, this.normalVector),
				material: this.material
			}
		}
	}

	class HitableList {
		constructor(hitables) {
			this.hitables = hitables
		}

		hit(ray, tmin, tmax) {
			let hit_record = null
			let closest_so_far = tmax
			for (let i = 0; i < this.hitables.length; i++) {
				let temp_rec = this.hitables[i].hit(ray, tmin, closest_so_far)
				if (temp_rec) {
					closest_so_far = temp_rec.t
					hit_record = temp_rec
				}
			}
			return hit_record
		}
	}

	(function(){
		let canvas = document.getElementById('canvas0')
		let ctx = canvas.getContext('2d')
		let width = canvas.width
		let height = canvas.height
		let imageData = ctx.getImageData(0, 0, width, height)
		let data = imageData.data
		const n_antialias = 8;
		const objects = new Array()
		objects[0] = new Sphere([0, 0, -1], 0.5, new Lambertian([0.8, 0.3, 0.3]))
		//objects.push(new Sphere([0, -100.5, -5], 100, new Lambertian([0.8, 0.8, 0])));
		objects.push(new Sphere([1, 0, -1], 0.5, new Metal([0.8, 0.6, 0.2])));
		objects.push(new Sphere([-1, 0, -1], 0.5, new Metal([0.8, 0.8, 0.8])));
		objects.push(new Plane(0, 1, 0, 0.5, new Metal([1, 1, 1])));
		const hitables = new HitableList(objects)
		camera = new Camera()
		for (let i = 0; i < width; i++) {
			console.log(i);
			for (let j = 0; j < height; j++) {
				let c = [0, 0, 0]
				for (let s = 0; s < n_antialias; s++) {
					const u = (i + Math.random()) / width
					const v = 1 - (j + Math.random()) / height
					ray = camera.getRay(u, v)
					p = ray.point_at_t(2.0)
					c = math.add(c, color(ray, hitables, 0))
				}
				c = math.divide(c, n_antialias)
				c = math.sqrt(c)
				const px = j * width + i
				data[px * 4 + 0] = 255 * c[0]
				data[px * 4 + 1] = 255 * c[1]
				data[px * 4 + 2] = 255 * c[2]
				data[px * 4 + 3] = 255
			}
		}
		ctx.putImageData(imageData, 0, 0)
	})()
</script>
</body>
</html>